media_pp\elements\source/
file_demuxer.rs1use std::{path::Path, sync::Arc, time::Duration};
2
3use crate::pp_log::{PpLog, pp_error, pp_info};
4use ffmpeg_next as ffmpeg;
5use thiserror::Error as ThisError;
6
7use crate::{
8 buffer::MediaBuffer,
9 bus::{Bus, BusEvent},
10 control::{ControlReceiver, drain_control},
11 element::{Element, ElementType, Source, SourceElement, element_pp_log},
12 pad::SrcPad,
13};
14
15#[derive(Debug, ThisError)]
18pub enum FileDemuxError {
19 #[error("ffmpeg error: {0}")]
20 Ffmpeg(#[from] ffmpeg::Error),
21}
22
23#[derive(Debug, Clone, Copy)]
26pub struct StreamInfo {
27 pub index: usize,
28 pub kind: ffmpeg::media::Type,
29}
30
31pub struct FileDemuxer {
40 pp_log: PpLog,
41 name: Arc<str>,
42 input: ffmpeg::format::context::Input,
43 pads: Vec<SrcPad>,
44 pending: Option<(usize, ffmpeg::Packet)>,
50}
51
52impl FileDemuxer {
53 pub fn open(
57 name: impl Into<String>,
58 path: impl AsRef<Path>,
59 ) -> Result<(Self, Vec<StreamInfo>), FileDemuxError> {
60 let input = ffmpeg::format::input(&path)?;
61
62 let streams: Vec<StreamInfo> = input
63 .streams()
64 .map(|s| StreamInfo {
65 index: s.index(),
66 kind: s.parameters().medium(),
67 })
68 .collect();
69
70 let pads = streams
71 .iter()
72 .map(|s| SrcPad::new(format!("src_{}", s.index)))
73 .collect();
74
75 let name: Arc<str> = name.into().into();
76 let pp_log = element_pp_log(ElementType::FileDemuxer, &name, None);
77 pp_info!(
78 pp_log: &pp_log,
79 "opened: path={}, {} stream(s)",
80 path.as_ref().display(),
81 streams.len()
82 );
83 Ok((
84 Self {
85 name,
86 pp_log,
87 input,
88 pads,
89 pending: None,
90 },
91 streams,
92 ))
93 }
94
95 pub fn stream_parameters(&self, index: usize) -> Option<ffmpeg::codec::Parameters> {
98 self.stream(index).map(|s| s.parameters())
99 }
100
101 pub fn stream_time_base(&self, index: usize) -> Option<ffmpeg::Rational> {
105 self.stream(index).map(|s| s.time_base())
106 }
107
108 fn stream(&self, index: usize) -> Option<ffmpeg::format::stream::Stream<'_>> {
109 self.input.streams().find(|s| s.index() == index)
110 }
111}
112
113impl Element for FileDemuxer {
114 fn name(&self) -> Arc<str> {
115 self.name.clone()
116 }
117
118 fn element_type(&self) -> ElementType {
119 ElementType::FileDemuxer
120 }
121
122 fn pp_log(&self) -> &PpLog {
123 &self.pp_log
124 }
125
126 fn pp_log_mut(&mut self) -> &mut PpLog {
127 &mut self.pp_log
128 }
129}
130
131impl Source for FileDemuxer {
132 fn src_pads(&mut self) -> &mut [SrcPad] {
133 &mut self.pads
134 }
135}
136
137impl SourceElement for FileDemuxer {
138 fn run(&mut self, control: &ControlReceiver, bus: &Bus) -> crate::error::Result<()> {
139 pp_info!(self, "started");
140 loop {
149 if drain_control(control, self, bus)?.stopped {
150 pp_info!(self, "stopped");
152 return Ok(());
153 }
154 let next = match self.pending.take() {
158 Some(next) => Some(next),
159 None => self.input.packets().next().map(|(s, p)| (s.index(), p)),
160 };
161 let Some((index, packet)) = next else {
162 break;
163 };
164 if let Some(pad) = self.pads.get_mut(index) {
165 if let Err(error) = pad.push(MediaBuffer::Packet(Arc::new(packet))) {
171 bus.post(
172 &self.pp_log,
173 BusEvent::Error {
174 element_type: ElementType::FileDemuxer,
175 name: self.name.clone(),
176 error,
177 },
178 );
179 }
180 }
181 }
182 for pad in self.pads.iter_mut() {
183 pad.push_eos(&self.pp_log)?;
184 }
185 pp_info!(self, "event=eos phase=source_completed outcome=ok");
186 Ok(())
187 }
188
189 fn seek(&mut self, target: Duration) -> crate::error::Result<Duration> {
190 let ts = target.as_micros().min(i64::MAX as u128) as i64;
202 self.input.seek(ts, ..).inspect_err(|error| {
203 pp_error!(self, "seek to {target:?} failed: {error}");
204 })?;
205
206 match self.input.packets().next() {
212 Some((stream, packet)) => {
213 let time_base = stream.time_base();
214 let landed = packet
215 .pts()
216 .or_else(|| packet.dts())
217 .map(|ts| ts_to_duration(ts, time_base))
218 .unwrap_or(Duration::ZERO);
219 self.pending = Some((stream.index(), packet));
220 Ok(landed)
221 }
222 None => Ok(target),
226 }
227 }
228}
229
230fn ts_to_duration(ts: i64, time_base: ffmpeg::Rational) -> Duration {
231 let secs = ts as f64 * f64::from(time_base.numerator()) / f64::from(time_base.denominator());
232 Duration::from_secs_f64(secs.max(0.0))
233}
234
235#[cfg(test)]
236mod tests {
237 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
238
239 use super::*;
240 use crate::control;
241 use crate::test_support::try_test_video;
242
243 struct CountingSink {
244 pp_log: PpLog,
245 count: Arc<AtomicUsize>,
246 saw_eos: Arc<AtomicBool>,
247 }
248
249 impl Element for CountingSink {
250 fn name(&self) -> Arc<str> {
251 "counting-sink".into()
252 }
253
254 fn element_type(&self) -> ElementType {
255 ElementType::Other
256 }
257
258 fn pp_log(&self) -> &PpLog {
259 &self.pp_log
260 }
261
262 fn pp_log_mut(&mut self) -> &mut PpLog {
263 &mut self.pp_log
264 }
265 }
266
267 impl crate::element::Sink for CountingSink {
268 fn consume(&mut self, buf: MediaBuffer) -> crate::error::Result<()> {
269 match buf {
270 MediaBuffer::Eos => self.saw_eos.store(true, Ordering::SeqCst),
271 _ => {
272 self.count.fetch_add(1, Ordering::SeqCst);
273 }
274 }
275 Ok(())
276 }
277
278 fn control(&mut self, _msg: crate::control::ControlMsg) -> crate::error::Result<()> {
279 Ok(())
280 }
281 }
282
283 #[test]
284 fn open_reports_stream_parameters_for_a_valid_index_and_none_out_of_range() {
285 let Some(path) = try_test_video() else { return };
286 let (demuxer, streams) = FileDemuxer::open("demux", &path).expect("open test video");
287 let video = streams
288 .iter()
289 .find(|s| s.kind == ffmpeg::media::Type::Video)
290 .expect("test video has a video stream");
291
292 assert!(demuxer.stream_parameters(video.index).is_some());
293 assert!(demuxer.stream_time_base(video.index).is_some());
294
295 let out_of_range = streams.len() + 1;
296 assert!(
297 demuxer.stream_parameters(out_of_range).is_none(),
298 "an out-of-range stream index must report nothing, not panic"
299 );
300 assert!(demuxer.stream_time_base(out_of_range).is_none());
301 }
302
303 #[test]
308 fn run_delivers_every_packet_on_a_linked_pad_then_eos() {
309 let Some(path) = try_test_video() else { return };
310 let (mut demuxer, streams) = FileDemuxer::open("demux", &path).expect("open test video");
311 let video = streams
312 .iter()
313 .find(|s| s.kind == ffmpeg::media::Type::Video)
314 .expect("test video has a video stream");
315
316 let count = Arc::new(AtomicUsize::new(0));
317 let saw_eos = Arc::new(AtomicBool::new(false));
318 demuxer.src_pads()[video.index].link(Box::new(CountingSink {
319 count: count.clone(),
320 saw_eos: saw_eos.clone(),
321 pp_log: element_pp_log(ElementType::Other, "counting-sink", None),
322 }));
323
324 let (bus, bus_rx) = Bus::new();
325 let (_tx, rx) = control::channel();
326 demuxer
327 .run(&rx, &bus)
328 .expect("run must reach eos cleanly, not error");
329
330 assert!(
331 count.load(Ordering::SeqCst) > 0,
332 "expected at least one packet delivered to the linked pad"
333 );
334 assert!(
335 saw_eos.load(Ordering::SeqCst),
336 "expected an Eos once the file is exhausted"
337 );
338 drop(bus);
339 assert!(
340 bus_rx.iter().all(|e| !matches!(e, BusEvent::Error { .. })),
341 "run must not report any errors demuxing a well-formed file"
342 );
343 }
344}